//
// Created by Pulsar on 2020/5/5.
//

#include <rc_network/rc_httpd.h>
#include <iostream>
#include <base/slog.hpp>
#include <string>
#include <map>

#define MAX_BUFFER 1024*10
#define ISspace(x) isspace((int)(x))

#define SERVER_STRING "Server: rchttpd/0.0.1\r\n"
#define STDIN   0
#define STDOUT  1
#define STDERR  2

namespace RC {
    namespace Network {
        namespace Httpd {
            enum {
                JSON = 0,
                IMAGE_JPEG,
                IMAGE_PNG,
                HTML,
                JS,
                CSS,
            };

            typedef struct _Request {
                int client;
                char method[255];
                char url[255];
                char path[512];
            } Request;

            typedef void (*url_function)(Request);

            std::map <std::string, url_function> function_map;

            /**
             * 绑定路由表
             * @param arg
             */
            void bind_function(std::string, void (*bind_function)(Httpd::Request));

            /**
             * 处理从套接字上监听到的一个 HTTP 请求
             * @param arg
             */
            void *accept_request(void *arg);

            /**
             * 返回给客户端这是个错误请求，400响应码
             * @param client
             */
            void bad_request(int client);

            /**
             * 执行控制命令
             * @param client
             * @param path
             * @param method
             * @param query_string
             */
            void excute_command(int client, const char *path, const char *method, const char *query_string);

            /**
             * 读取服务器上某个文件写到 socket 套接字
             * @param client
             * @param resource
             */
            void cat(int client, FILE *resource);

            /**
             * 处理发生在执行 cgi 程序时出现的错误
             */
            void cannot_execute(int client);

            /**
             * 把错误信息写到 perror
             */
            void error_die(const char *sc);

            /**
             * 运行cgi脚本
             * @param client
             * @param path
             * @param method
             * @param query_string
             */
            void execute_cgi(int client, const char *path,
                             const char *method, const char *query_string);

            /**
             * 读取一行HTTP报文
             * @param sock
             * @param buf
             * @param size
             * @return
             */
            int get_line(int sock, char *buf, int size);

            /**
             * 返回HTTP响应头
             */
            void headers(int client, const char *filename, const char *content_type);

            /**
             * 返回找不到请求文件
             */
            void not_found(int client);

            /**
             * 调用 cat 把服务器文件内容返回给浏览器。
             */
            void serve_file(int client, const char *filename, int header_type);

            /**
             * 开启http服务，包括绑定端口，监听，开启线程处理链接
             * @param port
             * @return
             */
            int startup(u_short *port);

            /**
             * 返回给浏览器表明收到的 HTTP 请求所用的 method 不被支持。
             */
            void unimplemented(int client);

            void render(int client, std::string tempfile);

        }

        void Httpd::render(int client, std::string tempfile) {

        }

        void Httpd::bind_function(std::string urlpath, void (*bind_function)(Httpd::Request)) {
            Httpd::function_map.insert(std::pair<std::string, Httpd::url_function>(urlpath, bind_function));
        }

        void *Httpd::accept_request(void *arg) {
            int client = (intptr_t) arg;
            char buf[MAX_BUFFER];
            size_t numchars;
            char method[255];
            char url[255];
            char path[512];
            size_t i, j;
            struct stat st;
            int cgi = 0;
            int header_type = HTML;


            char *query_string = NULL;

            numchars = get_line(client, buf, sizeof(buf));
            i = 0;
            j = 0;
            while (!ISspace(buf[i]) && (i < sizeof(method) - 1)) {
                method[i] = buf[i];
                i++;
            }
            j = i;
            method[i] = '\0';

            if (strcasecmp(method, "GET") && strcasecmp(method, "POST")) {
                unimplemented(client);
                return nullptr;
            }

            if (strcasecmp(method, "POST") == 0)
                cgi = 1;

            i = 0;
            while (ISspace(buf[j]) && (j < numchars))
                j++;
            while (!ISspace(buf[j]) && (i < sizeof(url) - 1) && (j < numchars)) {
                url[i] = buf[j];
                i++;
                j++;
            }
            url[i] = '\0';

            if (strcasecmp(method, "GET") == 0) {
                query_string = url;
                while ((*query_string != '?') && (*query_string != '\0'))
                    query_string++;
                if (*query_string == '?') {
                    cgi = 1;
                    *query_string = '\0';
                    query_string++;
                }
            }

            sprintf(path, "html%s", url);
            if (path[strlen(path) - 1] == '/')
                strcat(path, "index.html");
            if (stat(path, &st) == -1) {
                while ((numchars > 0) && strcmp("\n", buf)) {
                    numchars = get_line(client, buf, sizeof(buf));
                }
                not_found(client);
            } else {
                if ((st.st_mode & S_IFMT) == S_IFDIR) {
                    strcat(path, "/index.html");
                }
                std::string path_str = path;
                std::string file_extname = path_str.substr(path_str.find_last_of('.') + 1);
                if (file_extname == "html" ||
                    file_extname == "js" ||
                    file_extname == "css") {
                    if (file_extname == "html")
                        header_type = HTML;
                    if (file_extname == "js")
                        header_type = JS;
                    if (file_extname == "css")
                        header_type = CSS;

                } else {
                    if ((st.st_mode & S_IXUSR) ||
                        (st.st_mode & S_IXGRP) ||
                        (st.st_mode & S_IXOTH)) {
                        cgi = 1;
                    }
                }
                if (!cgi)
                    serve_file(client, path, header_type);
                else {
                    excute_command(client, path, method, query_string);
//                    execute_cgi(client, path, method, query_string);
                }
            }

            close(client);
        }

        void Httpd::bad_request(int client) {
            char buf[MAX_BUFFER];

            sprintf(buf, "HTTP/1.0 400 BAD REQUEST\r\n");
            send(client, buf, sizeof(buf), 0);
            sprintf(buf, "Content-type: text/html\r\n");
            send(client, buf, sizeof(buf), 0);
            sprintf(buf, "\r\n");
            send(client, buf, sizeof(buf), 0);
            sprintf(buf, "<P>Your browser sent a bad request, ");
            send(client, buf, sizeof(buf), 0);
            sprintf(buf, "such as a POST without a Content-Length.\r\n");
            send(client, buf, sizeof(buf), 0);
        }

        void Httpd::cat(int client, FILE *resource) {
            char buf[MAX_BUFFER];

            fgets(buf, sizeof(buf), resource);
            while (!feof(resource)) {
                send(client, buf, strlen(buf), 0);
                fgets(buf, sizeof(buf), resource);
            }
        }

        void Httpd::cannot_execute(int client) {
            char buf[MAX_BUFFER];
            sprintf(buf, "HTTP/1.0 500 Internal Server Error\r\n");
            send(client, buf, strlen(buf), 0);
            sprintf(buf, "Content-type: text/html\r\n");
            send(client, buf, strlen(buf), 0);
            sprintf(buf, "\r\n");
            send(client, buf, strlen(buf), 0);
            sprintf(buf, "<P>Error prohibited CGI execution.\r\n");
            send(client, buf, strlen(buf), 0);
        }

        void Httpd::error_die(const char *sc) {
            perror(sc);
            exit(1);
        }

        void Httpd::excute_command(int client, const char *path, const char *method, const char *query_string) {
            slog::info << "path:" << path << "method:" << method << "query_string:" << query_string << slog::endl;
            not_found(client);

//            if (function_map.find(url) == function_map.end()){
//            } else{
//                function_map[url](request);
//            }
        }

        void Httpd::execute_cgi(int client, const char *path, const char *method, const char *query_string) {
            slog::info << "Execute cgi" << slog::endl;
            char buf[MAX_BUFFER];
            int cgi_output[2];
            int cgi_input[2];
            pid_t pid;
            int status;
            int i;
            char c;
            int numchars = 1;
            int content_length = -1;

            buf[0] = 'A';
            buf[1] = '\0';
            if (strcasecmp(method, "GET") == 0)
                while ((numchars > 0) && strcmp("\n", buf))  /* read & discard headers */
                    numchars = get_line(client, buf, sizeof(buf));
            else if (strcasecmp(method, "POST") == 0) /*POST*/
            {
                numchars = get_line(client, buf, sizeof(buf));
                while ((numchars > 0) && strcmp("\n", buf)) {
                    buf[15] = '\0';
                    if (strcasecmp(buf, "Content-Length:") == 0)
                        content_length = atoi(&(buf[16]));
                    numchars = get_line(client, buf, sizeof(buf));
                }
                if (content_length == -1) {
                    bad_request(client);
                    return;
                }
            } else/*HEAD or other*/
            {
            }


            if (pipe(cgi_output) < 0) {
                cannot_execute(client);
                return;
            }
            if (pipe(cgi_input) < 0) {
                cannot_execute(client);
                return;
            }

            if ((pid = fork()) < 0) {
                cannot_execute(client);
                return;
            }
            sprintf(buf, "HTTP/1.0 200 OK\r\n");
            send(client, buf, strlen(buf), 0);
            if (pid == 0)  /* child: CGI script */
            {
                char meth_env[255];
                char query_env[255];
                char length_env[255];

                dup2(cgi_output[1], STDOUT);
                dup2(cgi_input[0], STDIN);
                close(cgi_output[0]);
                close(cgi_input[1]);
                sprintf(meth_env, "REQUEST_METHOD=%s", method);
                putenv(meth_env);
                if (strcasecmp(method, "GET") == 0) {
                    sprintf(query_env, "QUERY_STRING=%s", query_string);
                    putenv(query_env);
                } else {   /* POST */
                    sprintf(length_env, "CONTENT_LENGTH=%d", content_length);
                    putenv(length_env);
                }
                execl(path, NULL);
                exit(0);
            } else {    /* parent */
                close(cgi_output[1]);
                close(cgi_input[0]);
                if (strcasecmp(method, "POST") == 0)
                    for (i = 0; i < content_length; i++) {
                        recv(client, &c, 1, 0);
                        write(cgi_input[1], &c, 1);
                    }
                while (read(cgi_output[0], &c, 1) > 0)
                    send(client, &c, 1, 0);

                close(cgi_output[0]);
                close(cgi_input[1]);
                waitpid(pid, &status, 0);
            }
        }

        int Httpd::get_line(int sock, char *buf, int size) {
            int i = 0;
            char c = '\0';
            int n;

            while ((i < size - 1) && (c != '\n')) {
                n = recv(sock, &c, 1, 0);
                /* DEBUG printf("%02X\n", c); */
                if (n > 0) {
                    if (c == '\r') {
                        n = recv(sock, &c, 1, MSG_PEEK);
                        /* DEBUG printf("%02X\n", c); */
                        if ((n > 0) && (c == '\n'))
                            recv(sock, &c, 1, 0);
                        else
                            c = '\n';
                    }
                    buf[i] = c;
                    i++;
                } else
                    c = '\n';
            }
            buf[i] = '\0';
            return (i);
        }

        void Httpd::headers(int client, const char *filename, const char *content_type) {
            char buf[MAX_BUFFER];
            (void) filename;  /* could use filename to determine file type */

            strcpy(buf, "HTTP/1.0 200 OK\r\n");
            send(client, buf, strlen(buf), 0);
            strcpy(buf, SERVER_STRING);
            send(client, buf, strlen(buf), 0);
            sprintf(buf, "%s", content_type);
//            sprintf(buf, "Content-Type: text/html\r\n");
            send(client, buf, strlen(buf), 0);
            strcpy(buf, "\r\n");
            send(client, buf, strlen(buf), 0);
        }

        void Httpd::not_found(int client) {
            char buf[MAX_BUFFER];

            sprintf(buf, "HTTP/1.0 404 NOT FOUND\r\n");
            send(client, buf, strlen(buf), 0);
            sprintf(buf, SERVER_STRING);
            send(client, buf, strlen(buf), 0);
            sprintf(buf, "Content-Type: text/html\r\n");
            send(client, buf, strlen(buf), 0);
            sprintf(buf, "\r\n");
            send(client, buf, strlen(buf), 0);
            sprintf(buf, "<HTML><TITLE>Not Found</TITLE>\r\n");
            send(client, buf, strlen(buf), 0);
            sprintf(buf, "<BODY><P>The server could not fulfill\r\n");
            send(client, buf, strlen(buf), 0);
            sprintf(buf, "your request because the resource specified\r\n");
            send(client, buf, strlen(buf), 0);
            sprintf(buf, "is unavailable or nonexistent.\r\n");
            send(client, buf, strlen(buf), 0);
            sprintf(buf, "</BODY></HTML>\r\n");
            send(client, buf, strlen(buf), 0);
        }

        void Httpd::serve_file(int client, const char *filename, int header_type) {
            slog::wget << filename << slog::endl;
            FILE *resource = NULL;
            int numchars = 1;
            char buf[MAX_BUFFER];

            buf[0] = 'A';
            buf[1] = '\0';
            while ((numchars > 0) && strcmp("\n", buf))  /* read & discard headers */
                numchars = get_line(client, buf, sizeof(buf));

            resource = fopen(filename, "r");
            if (resource == NULL)
                not_found(client);
            else {
                switch (header_type) {
                    case JS:
                        headers(client, filename, "Content-Type: application/javascript\r\n");
                        break;
                    case CSS:
                        headers(client, filename, "Content-Type: text/css\r\n");
                        break;
                    default:
                        headers(client, filename, "Content-Type: text/html\r\n");
                        break;
                }
                cat(client, resource);
            }
            fclose(resource);
        }

        int Httpd::startup(u_short *port) {
            int httpd = 0;
            int on = 1;
            struct sockaddr_in name;

            httpd = socket(PF_INET, SOCK_STREAM, 0);
            if (httpd == -1)
                error_die("socket");
            memset(&name, 0, sizeof(name));
            name.sin_family = AF_INET;
            name.sin_port = htons(*port);
            name.sin_addr.s_addr = htonl(INADDR_ANY);
            if ((setsockopt(httpd, SOL_SOCKET, SO_REUSEADDR, &on, sizeof(on))) < 0) {
                error_die("setsockopt failed");
            }
            if (bind(httpd, (struct sockaddr *) &name, sizeof(name)) < 0)
                error_die("bind");
            if (*port == 0)  /* if dynamically allocating a port */
            {
                socklen_t namelen = sizeof(name);
                if (getsockname(httpd, (struct sockaddr *) &name, &namelen) == -1)
                    error_die("getsockname");
                *port = ntohs(name.sin_port);
            }
            if (listen(httpd, 5) < 0)
                error_die("listen");
            return (httpd);
        }

        void Httpd::unimplemented(int client) {
            char buf[MAX_BUFFER];

            sprintf(buf, "HTTP/1.0 501 Method Not Implemented\r\n");
            send(client, buf, strlen(buf), 0);
            sprintf(buf, SERVER_STRING);
            send(client, buf, strlen(buf), 0);
            sprintf(buf, "Content-Type: text/html\r\n");
            send(client, buf, strlen(buf), 0);
            sprintf(buf, "\r\n");
            send(client, buf, strlen(buf), 0);
            sprintf(buf, "<HTML><HEAD><TITLE>Method Not Implemented\r\n");
            send(client, buf, strlen(buf), 0);
            sprintf(buf, "</TITLE></HEAD>\r\n");
            send(client, buf, strlen(buf), 0);
            sprintf(buf, "<BODY><P>HTTP request method not supported.\r\n");
            send(client, buf, strlen(buf), 0);
            sprintf(buf, "</BODY></HTML>\r\n");
            send(client, buf, strlen(buf), 0);
        }


        void Httpd::HttpdServer::start_server(u_short port) {
            int server_sock = -1;
            int client_sock = -1;
            struct sockaddr_in client_name;
            socklen_t client_name_len = sizeof(client_name);
            pthread_t newthread;

            server_sock = startup(&port);
            printf("httpd running on port %d\n", port);

            while (1) {
                client_sock = accept(server_sock,
                                     (struct sockaddr *) &client_name,
                                     &client_name_len);
                if (client_sock == -1)
                    error_die("accept");
                /* accept_request(&client_sock); */
                if (pthread_create(&newthread, NULL, accept_request, (void *) (intptr_t) client_sock) != 0)
                    perror("pthread_create");
            }
            close(server_sock);
        }

        void Httpd::HttpdServer::send_command() {

        }
    }
}

